package threewings;

import java.awt.event.KeyEvent;

import javax.media.j3d.BranchGroup;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.vecmath.AxisAngle4f;
import javax.vecmath.Point3d;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

import com.sun.j3d.utils.geometry.Sphere;
import com.sun.j3d.utils.universe.SimpleUniverse;

/**
 * A Glider that the player controls
 *
 * @author Tyler Whitehouse.
 *         Created Oct 21, 2013.
 */
public class Glider {

	private Point3f position;
	private Point3f oldPos;
	private Vector3f velocity;
	private Vector3f forward;
	private float height;

	private BranchGroup bg;
	private TransformGroup tg;
	private Transform3D tran;
	private MyKeyListener keyListener = null;

	/**
	 * Creates a Glider attached to the universe specified
	 *
	 * @param u
	 */
	public Glider(SimpleUniverse u) {
		this.position = new Point3f(0f, 0f, 0f);
		this.oldPos = new Point3f(0f, 0f, 0f);
		this.velocity = new Vector3f(0f, 0f, 0f);
		this.forward = new Vector3f(0f, 0f, 1f);

		this.tg = new TransformGroup();
		this.tg.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		this.tg.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);

		this.tran = new Transform3D();
		this.tg.getTransform(this.tran);

		this.bg = new BranchGroup();

		this.addGliderShape();

		this.bg.addChild(this.tg);

		u.addBranchGraph(this.bg);
	}

	/**
	 * Adds the keylistener to this
	 *
	 * @param k
	 */
	public void setKeyListener(MyKeyListener k) {
		this.keyListener = k;
	}
	/**
	 * Updates the glider, called in the main loop
	 *
	 */
	public void update() {

		Vector3f newVel = this.getVelocity();

		/**** START USER INPUT ****/
		// rotate left
		if (this.keyListener.left()) {
			Vector3f forward = this.getForward();
			double angle = Math.atan2(forward.z, forward.x);
			angle -= 0.01;
			this.forward.set((float) Math.cos(angle), forward.y,
					(float) Math.sin(angle));
			this.forward.normalize();
		}

		// rotate right
		if (this.keyListener.right()) {
			Vector3f forward = this.getForward();
			double angle = Math.atan2(forward.z, forward.x);
			angle += 0.01;
			this.forward.set((float) Math.cos(angle), forward.y,
					(float) Math.sin(angle));
			this.forward.normalize();
		}

		if (this.keyListener.up()) {
			this.position.y -= 0.5f;
		}
		if (this.keyListener.down()) {
			this.position.y += 0.5f;
		}
		if (this.keyListener.space()) {
			Vector3f forwardThrust = this.getForward();
			forwardThrust.scale(0.005f);
			newVel.add(forwardThrust);
		}

		// add thrust
		Vector3f forwardThrust = this.getForward();
		forwardThrust.scale(0.001f);
		// newVel.add(forwardThrust);

		newVel.scale(0.95f);
		/**** END USER INPUT ****/

		/**** START PHYSICS CALCULATIONS ****/
		// gravity
		newVel.y -= 0.005f;

		// normal force
		Vector3f norm = Chunk.getNormal(this.position.x, this.position.z);
		float tHeight = Chunk.getHeight(this.position.x, this.position.z);
		float dy = this.position.y - tHeight;
		if (dy > 1)
			norm.scale(0.0001f / (dy * dy * dy));
		else
			norm.scale(0.0001f);
		newVel.add(norm);

		/**** END PHYSICS CALCULATIONS ****/

		// move glider
		this.velocity = newVel;
		this.moveBy(this.velocity);

		// make sure that the glider doesn't go under
		if (this.position.y - 0.1f < Chunk.getHeight(this.position.x, this.position.z)) {
			this.position.y = Chunk.getHeight(this.position.x, this.position.z) + 0.1f;
		}

		// point the glider
		Vector3f toPoint = this.getForward();
		toPoint.add(this.position);
		this.pointAt(new Point3f(toPoint));
	}

	public void update(float dt){
		this.update(dt, 1f);
	}
	
	/**
	 * Updates the glider in terms of time
	 * 
	 * @param dt
	 * @param timeFactor
	 */
	public void update(float dt, float timeFactor){
		if (dt == 0) return;
		//Vector3f forceApplied = this.getVelocity();
		//forceApplied.scale(0.95f);
		Vector3f forceApplied = new Vector3f();
		/**** START USER INPUT ****/
			// rotate left
			if(this.keyListener.isPressed(KeyEvent.VK_LEFT)){
				Vector3f forward = this.getForward();
				double angle = Math.atan2(forward.z, forward.x);
				angle-=30f*dt;
				this.forward.set((float)Math.cos(angle), forward.y, (float)Math.sin(angle));
				this.forward.normalize();
			}
			
			// rotate right
			if(this.keyListener.isPressed(KeyEvent.VK_RIGHT)){
				Vector3f forward = this.getForward();
				double angle = Math.atan2(forward.z, forward.x);
				angle+=30f*dt;
				this.forward.set((float)Math.cos(angle), forward.y, (float)Math.sin(angle));
				this.forward.normalize();
			}
			
			// add down force
			if (this.keyListener.isPressed(KeyEvent.VK_UP)){
				forceApplied.y -= 5;
			}
			
			// add thrust force
			Vector3f forwardThrust = this.getForward();
			if (this.keyListener.isPressed(KeyEvent.VK_F)){
				forwardThrust.scale(1f);;
			} else {
				forwardThrust.scale(0.1f);
			}
			forceApplied.add(forwardThrust);
			
		/**** END USER INPUT ****/
		
		
		
		/**** START PHYSICS CALCULATIONS ****/
			// gravity
			forceApplied.y -= 10f;
			
			
			
			// up force from wings
			// TODO maybe make this depend on forward speed?
			forceApplied.y += 8f;
			
			
			
			// drag force
			Vector3f drag = this.getVelocity();
			// opposite direction and a percentage of the velocity
			drag.scale(-0.5f);
			forceApplied.add(drag);
			
			
			
			// normal force
			Vector3f norm = Chunk.getNormal(this.position.x, this.position.z);
			// projection of A onto B = (A dot unitVectorB)*unitVectorB
			// projection of the resultant onto the normal = (R dot normal)*normal
			Vector3f resultant = new Vector3f(forceApplied);
			norm.scale(-resultant.dot(norm));
			
			float tHeight = Chunk.getHeight(this.position.x, this.position.z);
			float dy = this.position.y - tHeight + 0.1f;
			// scale for if it is close
			if (dy < 0.7){
				norm.scale(1f/(dy*dy*dy));
				if (this.velocity.y > 0){
					norm.scale(0.5f);
				}
			}else {
				norm.scale(0f);
			}
			
			forceApplied.add(norm);
			
			
			
			
			/* Transfer Force to Velocity
			 * 
			 * m = 1
			 * f = m*a = a
			 * v = a*t
			 * v = f*t
			 */
			Vector3f newVel = new Vector3f(forceApplied);
			newVel.scale(dt);
			Vector3f oldVel = this.getVelocity();
			//oldVel.scale(timeFactor);
			newVel.add(oldVel);
		/**** END PHYSICS CALCULATIONS ****/
		
		
		
		// move glider
		this.velocity = newVel;
		Vector3f moveBy = this.getVelocity();
		moveBy.scale(timeFactor);
		this.moveBy(moveBy);
		
		// make sure that the glider doesn't go under
		if (this.position.y - 0.5 < Chunk.getHeight(this.position.x, this.position.z)){
			this.position.y = Chunk.getHeight(this.position.x, this.position.z) + 0.5f;
		}
		
		// point the glider
		Vector3f toPoint = this.getForward();
		toPoint.add(this.position);
		this.pointAt(new Point3f(toPoint));
	}
	
	
	/**
	 * Set the position of the glider
	 * 
	 * @param x
	 * @param y
	 * @param z
	 */
	public void setPosition(float x, float y, float z) {
		this.oldPos.set(this.position);
		this.position.set(x, y, z);
		this.tran.setTranslation(new Vector3f(this.position));
		this.tg.setTransform(this.tran);
	}

	/**
	 * Set the position of the glider
	 * 
	 * @param pos
	 */
	public void setPosition(Point3f pos) {
		this.oldPos.set(this.position);
		this.position.set(pos);
		this.tran.setTranslation(new Vector3f(this.position));
		this.tg.setTransform(this.tran);
	}

	/**
	 * Move the position of the glider by the parameters
	 * 
	 * @param x
	 * @param y
	 * @param z
	 */
	public void moveBy(float x, float y, float z) {
		Vector3f diff = new Vector3f(x, y, z);
		this.oldPos.set(this.position);
		this.position.add(diff);
		this.tran.setTranslation(new Vector3f(this.position));
		this.tg.setTransform(this.tran);
	}

	/**
	 * Move the position of the glider by the vector
	 * @param diff 
	 * 
	 */
	public void moveBy(Vector3f diff) {
		this.oldPos.set(this.position);
		this.position.add(diff);
		this.tran.setTranslation(new Vector3f(this.position));
		this.tg.setTransform(this.tran);
	}

	/**
	 * Set the rotation of the camera around the given axis using right hand
	 * rule
	 * 
	 * @param aa
	 *            , AxisAngle4d
	 */
	public void setRotation(AxisAngle4f aa) {
		this.tran.setRotation(aa);
		this.tg.setTransform(this.tran);
	}

	/**
	 * return a copy of the position of the glider
	 * 
	 * @return position of the glider
	 */
	public Point3f getPosition() {
		return new Point3f(this.position);
	}

	/**
	 * return a copy of the velocity of the glider
	 * 
	 * @return Vector3f velocity
	 */
	public Vector3f getVelocity() {
		return new Vector3f(this.velocity);
	}

	/**
	 * return a copy of the forward vector
	 * 
	 * @return Vector3f forward
	 */
	public Vector3f getForward() {
		return new Vector3f(this.forward);
	}

	/**
	 * Point the Glider at a specific point
	 * @param c 
	 */
	public void pointAt(Point3f c) {
		// record current position
		Vector3f oldpos = new Vector3f(this.position);

		Point3d pos = new Point3d(this.position);
		Point3d cd = new Point3d(c);
		this.tran.lookAt(pos, cd, new Vector3d(0, 1, 0));
		this.tran.invert();

		// reset to correct position
		this.tran.setTranslation(oldpos);

		this.tg.setTransform(this.tran);

		// set forward vector
		oldpos.scale(-1f);
		oldpos.add(c);
		oldpos.normalize();
		this.forward = oldpos;
	}

	private void addGliderShape() {
		// make glider
		TransformGroup tg1 = new TransformGroup();
		TransformGroup tg2 = new TransformGroup();
		Transform3D tf1 = new Transform3D();
		Transform3D tf2 = new Transform3D();
		Sphere s1 = new Sphere(0.25f);
		Sphere s2 = new Sphere(0.5f);
		tf1.setTranslation(new Vector3f(0, 0, 0.5f));
		tf2.setTranslation(new Vector3f(0, 0, -0.5f));
		tg1.setTransform(tf1);
		tg2.setTransform(tf2);
		tg1.addChild(s1);
		tg2.addChild(s2);

		this.tg.addChild(tg1);
		this.tg.addChild(tg2);
		
		this.height = 0.25f;
	}
}
